package org.glandais.profiler;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.management.ManagementFactory;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;

import org.glandais.profiler.beans.MethodTrace;
import org.glandais.profiler.beans.ProcessingList;
import org.glandais.profiler.beans.TraceStack;

public class TraceRecorder {

	private static final String INTERO = "?"; //$NON-NLS-1$
	private static final String DOT_ORIG_XML = ".orig.xml"; //$NON-NLS-1$
	private static final String DEUX_POINTS = " : "; //$NON-NLS-1$
	private static final String DOT_XML = ".xml"; //$NON-NLS-1$
	private static final String INDENT = "-"; //$NON-NLS-1$
	private static final String EXIT_RAW = "exitRaw"; //$NON-NLS-1$
	private static final String SPACE = " "; //$NON-NLS-1$
	private static final String ENTER_RAW = "enterRaw"; //$NON-NLS-1$
	private static final String HOUR_FORMAT = "HH-mm-ss.SSS"; //$NON-NLS-1$
	private static final ArrayList<TraceStack> stacks = new ArrayList<TraceStack>();
	private static final SimpleDateFormat SIMPLE_DATE_FORMAT_FILE = new SimpleDateFormat(HOUR_FORMAT);

	private static synchronized void addTraceStack(TraceStack traceStack) {
		stacks.add(traceStack);
		if (Agent.configBean.isLogVerbose()) {
			Agent.configBean.logVerbose(Messages.getString("TraceRecorder.STACKS_COUNT") + stacks.size()); //$NON-NLS-1$
		}
	}

	private static final ThreadLocal<TraceStack> callStackTL = new ThreadLocal<TraceStack>() {
		@Override
		protected TraceStack initialValue() {
			return new TraceStack();
		}
	};
	private static ThreadLocal<Boolean> logging = new ThreadLocal<Boolean>() {
		@Override
		protected Boolean initialValue() {
			return Boolean.TRUE;
		}
	};

	public static final ThreadLocal<ProcessingList> totalTimeTL = new ThreadLocal<ProcessingList>() {
		@Override
		protected ProcessingList initialValue() {
			return new ProcessingList();
		}
	};

	private static final String hashCode(Object called) {
		return Integer.toHexString(System.identityHashCode(called));
	}

	public static final void enter(String type, Object called, String signature) {
		totalTimeTL.get().startProcessing();
		enterRaw(type, called, signature);
		totalTimeTL.get().stopProcessing();
	}

	public static final void exit(String type, Object called, String signature) {
		totalTimeTL.get().startProcessing();
		exitRaw(type, called, signature);
		totalTimeTL.get().stopProcessing();
	}

	private static final void enterRaw(String type, Object called, String signature) {
		if (logging.get()) {
			logging.set(Boolean.FALSE);
			try {
				TraceStack traceStack = callStackTL.get();
				if (Agent.configBean.isLogTraceMethodLogs()) {
					Agent.configBean.logTraceMethodLogs(Messages.getString("TraceRecorder.ENTERING") + signature); //$NON-NLS-1$
				}
				boolean doTrace = true;
				MethodTrace traceMethod = null;
				if (traceStack.stack.empty()) {
					traceStack.stackStart = System.currentTimeMillis();
					traceStack.processingNanos = totalTimeTL.get();
					if (Agent.configBean.getEntryMethods().size() > 0) {
						doTrace = false;
						for (String entryMethod : Agent.configBean.getEntryMethods()) {
							if (!doTrace && signature.contains(entryMethod)) {
								if (Agent.configBean.isLogTraceMethodLogs()) {
									Agent.configBean.logTraceMethodLogs(Messages
											.getString("TraceRecorder.ENTRY_METHOD") + signature); //$NON-NLS-1$
								}
								doTrace = true;
							}
						}
					}
				}
				if (doTrace) {
					traceMethod = new MethodTrace();
					if (!traceStack.stack.empty()) {
						traceStack.stack.peek().subMethods.add(traceMethod);
					} else {
						traceStack.entryMethod = traceMethod;
					}
					traceStack.stack.push(traceMethod);

					traceMethod.called = hashCode(called);
					traceMethod.signature = signature;
					traceMethod.type = type;
					traceMethod.startSystemNanoTime = System.nanoTime();
				}
			} catch (Throwable e) {
				if (Agent.configBean.isLogError()) {
					Agent.configBean.logError(ENTER_RAW, e);
				}
			} finally {
				logging.set(Boolean.TRUE);
			}
		}
	}

	private static final void exitRaw(String type, Object called, String signature) {
		if (logging.get()) {
			logging.set(Boolean.FALSE);
			try {
				TraceStack traceStack = callStackTL.get();
				if (Agent.configBean.isLogTraceMethodLogs()) {
					Agent.configBean.logTraceMethodLogs(Messages.getString("TraceRecorder.EXITING") + signature); //$NON-NLS-1$
				}
				if (!traceStack.stack.empty()) {
					MethodTrace traceMethod = traceStack.stack.pop();
					traceMethod.durationNano = System.nanoTime() - traceMethod.startSystemNanoTime;
					if (traceStack.stack.isEmpty()) {
						callStackTL.set(new TraceStack());
						if (traceMethod.getDurationMillis() >= Agent.configBean.getMinLengthMsGlobal()) {
							if (Agent.configBean.isLogTraceMethodLogs()) {
								Agent.configBean
										.logTraceMethodLogs(Messages.getString("TraceRecorder.STACK_FINISHED") + traceMethod.getDurationMillis() //$NON-NLS-1$
												+ SPACE + signature);
							}
							addTraceStack(traceStack);
							if (Agent.configBean.isSaveSnapshots()) {
								saveStack(traceStack);
							}
						}
					} else {
						if (traceMethod.getDurationMillis() < Agent.configBean.getMinLengthMsMethod()) {
							if (Agent.configBean.isLogTraceMethodLogs()) {
								Agent.configBean
										.logTraceMethodLogs(Messages.getString("TraceRecorder.TOO_SHORT") + traceMethod.getDurationMillis() //$NON-NLS-1$
												+ SPACE + signature);
							}
							traceStack.stack.peek().subMethods.remove(traceMethod);
						}
					}
				}
			} catch (Throwable e) {
				if (Agent.configBean.isLogError()) {
					Agent.configBean.logError(EXIT_RAW, e);
				}
			} finally {
				logging.set(Boolean.TRUE);
			}
		}
	}

	// private static final String getStringDate(Date date) {
	// return SIMPLE_DATE_FORMAT_FILE.format(date);
	// }

	private static final String getStringDate(long date) {
		return SIMPLE_DATE_FORMAT_FILE.format(new Date(date));
	}

	private static final void saveStack(TraceStack traceStack) {
		String startTime = getStringDate(ManagementFactory.getRuntimeMXBean().getStartTime());

		File subFolder = new File(Agent.configBean.getDirectory(), startTime);
		String startTimeMeth = getStringDate(traceStack.stackStart);
		String l = Long.toString(traceStack.entryMethod.durationNano);
		String fileName = startTimeMeth + INDENT + l + DOT_XML;

		if (Agent.configBean.isLogVerbose()) {
			Agent.configBean
					.logVerbose(Messages.getString("TraceRecorder.SAVE_STACK_TO") + fileName + DEUX_POINTS + traceStack); //$NON-NLS-1$
		}

		File newFile = new File(subFolder, fileName);
		try {
			saveObject(traceStack, newFile);
		} catch (Exception e) {
			System.err.println(Messages.getString("TraceRecorder.FAILED_SAVE_STACK")); //$NON-NLS-1$
			e.printStackTrace(System.err);
		}
	}

	@SuppressWarnings("unchecked")
	public static final void shutdown() {
		logging = new ThreadLocal<Boolean>() {
			@Override
			protected Boolean initialValue() {
				return Boolean.FALSE;
			}
		};
		ClassFileTransformer.doTransformClasses = false;

		ArrayList<TraceStack> stacksCopy = (ArrayList<TraceStack>) stacks.clone();
		String startTime = getStringDate(ManagementFactory.getRuntimeMXBean().getStartTime());

		String fileName = startTime + DOT_ORIG_XML;
		if (Agent.configBean.isLogVerbose()) {
			Agent.configBean.logVerbose(Messages.getString("TraceRecorder.SAVE_STACKS_TO") + fileName); //$NON-NLS-1$
			Agent.configBean.logVerbose(Messages.getString("TraceRecorder.STACKS_COUNT") + stacksCopy.size()); //$NON-NLS-1$
		}

		File f = new File(new File(Agent.configBean.getDirectory()), fileName);
		try {
			saveObject(stacksCopy, f);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}

		String fileNameProcessed = startTime + DOT_XML;
		if (Agent.configBean.isLogVerbose()) {
			Agent.configBean
					.logVerbose(Messages.getString("TraceRecorder.SAVE_PROCESSED_STACKS_TO") + fileNameProcessed); //$NON-NLS-1$
			Agent.configBean.logVerbose(Messages.getString("TraceRecorder.STACKS_COUNT") + stacksCopy.size()); //$NON-NLS-1$
		}
		for (TraceStack traceStack : stacksCopy) {
			traceStack.removeProcessing();
		}

		File fProcessed = new File(new File(Agent.configBean.getDirectory()), fileNameProcessed);
		try {
			saveObject(stacksCopy, fProcessed);
		} catch (IOException e) {
			throw new RuntimeException(e);
		}

	}

	private static final void saveObject(Object o, File f) throws FileNotFoundException, IOException {
		if (!f.getParentFile().exists()) {
			f.getParentFile().mkdirs();
		}

		FileOutputStream fos = new FileOutputStream(f);
		ObjectOutputStream xOOS = Agent.xstream.createObjectOutputStream(fos);
		xOOS.writeObject(o);
		xOOS.close();
	}

}
